home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 7661 / 7661.xpi / components / RILdelegate.js < prev    next >
Text File  |  2009-12-24  |  38KB  |  1,251 lines

  1. /*
  2.  
  3. License: This source code may not be used in other applications whether they
  4. be personal, commercial, free, or paid without written permission from Read It Later.
  5.  
  6.  
  7. /////////
  8. DEVELOPER API: readitlaterlist.com/api/
  9. /////////
  10.  
  11. If you would like to customize Read It Later or build an application that works with
  12. Read it Later take a look at the READ IT LATER OPEN API:
  13. http://readitlaterlist.com/api/
  14.  
  15. Suggestions for additions to Read It Later are VERY welcome.  A large number of user
  16. suggestions have been implemented.  Please let me know of any additional features you
  17. are seeking at: http://readitlaterlist.com/support/
  18.  
  19. Thanks
  20.  
  21. */
  22.  
  23. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  24.  
  25.  
  26. function RILdelegate()
  27. {
  28.     this.wrappedJSObject = this;
  29.     
  30.     this.v = '2.0.3';
  31.     this.upgradedUrl = 'http://readitlaterlist.com/upgraded/'; //changelog address
  32.     this.upgradedMinorUrl = 'http://readitlaterlist.com/upgraded-minor/'; //changelog address
  33.     this.installedUrl = 'http://readitlaterlist.com/installed/'; //installed address
  34.     
  35.     this.databaseName = 'readItLater.sqlite';
  36.     
  37.     // time in MILLISECONDS
  38.     this.timeToAllowLinkResolverBeforeSavingLink = 15     * 1000;
  39.     this.timeToWaitBeforeFlushingScrollPositions = 2     * 1000;
  40.     
  41.     // time in SECONDS
  42.     this.idleTimeAutoSyncTrigger = 3 * 60 * 60; // modifying this may cause rate limit issues for your account, please be careful
  43.         
  44.     this.numberOfMostUsedTags = 4;
  45.     
  46.     this.loginInfo = {
  47.     hostname: 'chrome://isreaditlater',
  48.     formSubmitURL: null,
  49.     httprealm: 'Account login'
  50.     };
  51.     
  52.     this.errorPackages = {};
  53.     this.checkedFavIcons = [];
  54.     
  55.     //
  56.     this.channels = {};
  57.     
  58. }
  59.  
  60. // class definition
  61. RILdelegate.prototype = {
  62.  
  63.     // properties required for XPCOM registration:
  64.     classDescription: "Read It Later App Delegate Javascript XPCOM Component",
  65.     classID:          Components.ID("{38247750-aab5-11de-8a39-0800200c9a66}"),
  66.     contractID:       "@ril.ideashower.com/rildelegate;1",
  67.     
  68.     QueryInterface: XPCOMUtils.generateQI(),
  69.     
  70.     //////////////////////////////////////////////////
  71.     
  72.    
  73.     
  74.     init : function() {
  75.     
  76.         
  77.     if (!this.inited)
  78.     {
  79.         
  80.         // -- Loading and Starting -- //
  81.         this.connectToCoreServices();        
  82.     
  83.         // Connect to db
  84.         this.DB = this.connectToDatabase(this.databaseName);
  85.         
  86.         // -- Installing and Upgrading -- //
  87.         this.install();
  88.         
  89.         // Grab list
  90.         this.LIST.fetch();              
  91.         
  92.         // Add idle auto-sync
  93.         this.IDLE.addIdleObserver(this.autoSyncIdleObserver, this.idleTimeAutoSyncTrigger);        
  94.         
  95.         this.inited = true;
  96.         
  97.         this.definePrototypes();
  98.     }
  99.         
  100.     },
  101.     
  102.     connectToCoreServices : function() {
  103.             
  104.         Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  105.         
  106.         // Firefox        
  107.         this.PROMPT = Components
  108.                 .classes["@mozilla.org/embedcomp/prompt-service;1"]
  109.                 .getService(Components.interfaces.nsIPromptService);
  110.                 
  111.         this.JSON = Components.classes["@mozilla.org/dom/json;1"]
  112.                 .createInstance(Components.interfaces.nsIJSON);
  113.         
  114.         this.STORAGE = Components.classes["@mozilla.org/storage/service;1"]
  115.                 .getService(Components.interfaces.mozIStorageService);
  116.         
  117.         this.OBSERVER = Components.classes["@mozilla.org/observer-service;1"]
  118.                 .getService(Components.interfaces.nsIObserverService);
  119.         
  120.     this.IO = Components.classes["@mozilla.org/network/io-service;1"]
  121.         .getService(Components.interfaces.nsIIOService);
  122.                 
  123.     this.ICO = Components
  124.         .classes["@mozilla.org/browser/favicon-service;1"]
  125.         .getService(Components.interfaces.nsIFaviconService);
  126.         
  127.     this.LOGIN = Components.classes["@mozilla.org/login-manager;1"]
  128.                     .getService(Components.interfaces.nsILoginManager);
  129.             
  130.     this.IDLE = Components.classes["@mozilla.org/widget/idleservice;1"]
  131.                     .getService(Components.interfaces.nsIIdleService)
  132.         
  133.         
  134.         // Read It Later
  135.         this.PREFS = Components.classes['@ril.ideashower.com/rilprefs;1'].getService().wrappedJSObject;
  136.         
  137.         this.LIST = Components.classes['@ril.ideashower.com/rillist;1'].getService().wrappedJSObject;
  138.         this.LIST.init();
  139.         
  140.         this.SYNC = Components.classes['@ril.ideashower.com/rilsync;1'].getService().wrappedJSObject;
  141.         this.SYNC.init();
  142.         
  143.         this.ASSETS = Components.classes['@ril.ideashower.com/rilassetmanager;1'].createInstance(Components.interfaces.nsIRILassetManager);       
  144.         this.ASSETS.init();
  145.         
  146.         this.OFFLINE = Components.classes['@ril.ideashower.com/rilofflinequeue;1'].getService().wrappedJSObject;
  147.     this.OFFLINE.init();        
  148.         
  149.     },
  150.     
  151.     
  152.     // -- Database Connections -- //
  153.     
  154.     connectToDatabase : function(databaseName) {
  155.         
  156.         let file = Components.classes["@mozilla.org/file/directory_service;1"]
  157.                         .getService(Components.interfaces.nsIProperties)
  158.                         .get("ProfD", Components.interfaces.nsIFile);
  159.         file.append(databaseName);
  160.         
  161.         return this.STORAGE.openDatabase(file);
  162.     },
  163.     
  164.     createTables : function() {
  165.  
  166.     if (!this.DB.tableExists('items'))
  167.     {
  168.         this.DB.executeSimpleSQL("CREATE TABLE items (item_id INTEGER NOT NULL , unique_id INTEGER NOT NULL, url VARCHAR NOT NULL , title VARCHAR NOT NULL , time_updated INTEGER NOT NULL , offline_web INTEGER NOT NULL , offline_text INTEGER NOT NULL, percent INTEGER NOT NULL  )");
  169.         this.DB.executeSimpleSQL("CREATE INDEX item_idIndex ON items ( item_id )");
  170.         this.DB.executeSimpleSQL("CREATE UNIQUE INDEX url ON items ( url )");
  171.         this.DB.executeSimpleSQL("CREATE INDEX time_updated ON items ( time_updated )");
  172.         this.DB.executeSimpleSQL("CREATE INDEX percent ON items ( percent )");
  173.     }
  174.     
  175.     if (!this.DB.tableExists('tags'))
  176.     {
  177.         this.DB.executeSimpleSQL("CREATE TABLE tags (item_id INTEGER NOT NULL , tag VARCHAR NOT NULL , PRIMARY KEY (item_id, tag))");
  178.     }
  179.     
  180.     if (!this.DB.tableExists('scroll'))
  181.     {
  182.         this.DB.executeSimpleSQL("CREATE TABLE scroll (item_id INTEGER NOT NULL, view INTEGER NOT NULL, section INTEGER NOT NULL, page INTEGER NOT NULL, node_index INTEGER NOT NULL, percent INTEGER NOT NULL, time_updated INTEGER NOT NULL, PRIMARY KEY (item_id, view))");
  183.     }
  184.     
  185.     if (!this.DB.tableExists('resolver'))
  186.     {
  187.         this.DB.executeSimpleSQL("CREATE TABLE resolver (item_id INTEGER NOT NULL , url VARCHAR)");
  188.     }
  189.     
  190.     if (!this.DB.tableExists('sync_queue'))
  191.     {
  192.         this.DB.executeSimpleSQL("CREATE TABLE sync_queue (type VARCHAR NOT NULL, url VARCHAR NOT NULL, PRIMARY KEY(url, type))");
  193.     }
  194.     
  195.     if (!this.DB.tableExists('assets'))
  196.     {
  197.         this.DB.executeSimpleSQL("CREATE TABLE assets (asset_domain VARCHAR NOT NULL)");
  198.     }
  199.     
  200.     if (!this.DB.tableExists('assets_items'))
  201.     {
  202.         this.DB.executeSimpleSQL("CREATE TABLE assets_items (asset_domain VARCHAR NOT NULL , item_id INTEGER NOT NULL , PRIMARY KEY (asset_domain, item_id))");        
  203.         this.DB.executeSimpleSQL("CREATE INDEX item_id_lookup ON assets_items ( item_id )");
  204.     }
  205.     
  206.     if (!this.DB.tableExists('vars'))
  207.     {
  208.         this.DB.executeSimpleSQL("CREATE TABLE vars (unique_id INTEGER NOT NULL)");
  209.         this.DB.executeSimpleSQL("INSERT INTO vars (unique_id) VALUES (0)");
  210.     }
  211.  
  212.     },
  213.     
  214.     dumpAndReinstallDatabase : function()
  215.     {
  216.     // Drop all tables - ideally we'd just remove the file but this.DB.close() results in an error, so we do it the long way
  217.     let sql = "select name from sqlite_master where type = 'table'";
  218.     let statement = this.DB.createStatement(sql);
  219.     let dropStatment;
  220.     let drops = [];
  221.     try {                    
  222.         while (statement.step())
  223.         {        
  224.         dropStatement = "DROP TABLE " + statement.row.name;
  225.         drops.push(dropStatement);
  226.         } 
  227.     } catch(e) {
  228.         Components.utils.reportError(e);   
  229.     }    
  230.     finally {
  231.         statement.reset();
  232.     }
  233.     
  234.     // Clear space
  235.     this.DB.executeSimpleSQL("VACUUM");
  236.     
  237.     // drop
  238.     for(let i=0; i<drops.length; i++)
  239.     {
  240.         this.DB.executeSimpleSQL(drops[i]);
  241.     }
  242.  
  243.     this.createTables();
  244.     },
  245.     
  246.     
  247.     // -- Install / Upgrades -- //
  248.     
  249.     install : function() {        
  250.         
  251.     this.PREFS.loadDefaults();
  252.         this.createTables();
  253.     
  254.     
  255.     // -- Upgrades
  256.     
  257.     let justInstalled = false;
  258.     
  259.     // -- If First Run -- //
  260.     if (!this.PREFS.getBool('installed')) {
  261.         
  262.         //Set this version as original install point
  263.         this.PREFS.set('install-version', this.v);
  264.         
  265.         //Set as installed
  266.         this.PREFS.set('installed', true);
  267.         
  268.         justInstalled=true;
  269.     }
  270.     
  271.     // --- Check Version and show Changelog on update --- //
  272.     if (this.PREFS.get('version') != this.v) {
  273.         if (justInstalled)
  274.             {
  275.                 this.openLoginWhenStarted = true;    
  276.         } else
  277.             {
  278.                 if (this.PREFS.get('version') < '2.0.1')
  279.                     this.setTimeout(function(){this.openUrl(this.upgradedUrl, false, 'tab');}, 750);
  280.                 else
  281.                     this.setTimeout(function(){this.openUrl(this.upgradedMinorUrl, false, 'tab');}, 750);                
  282.         }
  283.         this.PREFS.set('version', this.v);
  284.     }
  285.         
  286.     },
  287.     
  288.     
  289.     
  290.     
  291.     
  292.     
  293.     // -- Fetching List -- //
  294.     
  295.     listHasBeenReloaded : function(success)
  296.     {
  297.     
  298.     if (!this.listHasBeenLoadedOnce)
  299.     {
  300.         this.listHasBeenLoadedOnce = true;
  301.         this.upgradeFromBeta();
  302.     }
  303.     
  304.     if (!this.hasSyncedAtStartup)
  305.     {
  306.         this.hasSyncedAtStartup = true;
  307.         if (!this.startupError && !this.listError && this.PREFS.getBool('autoSync') && this.SYNC.syncingEnabled())
  308.             this.SYNC.sync();
  309.     }
  310.     
  311.         this.commandInAllOpenWindows('RIL', 'checkPage', null, true);
  312.         this.refreshListInAllOpenWindows();
  313.     
  314.     },
  315.     
  316.     filterList : function(typeOfList, filter, delegate)
  317.     {
  318.     if (!this.LIST || !this.LIST.list) return false;
  319.     
  320.     // Determine which list to use as base
  321.     let listSource;
  322.     switch(typeOfList)
  323.         {
  324.         case('current'):
  325.         this.LIST.getCurrentList();
  326.         listSource = this.LIST.currentList;
  327.         break;
  328.         case('read'):
  329.                 // should never get right here??
  330.                 listSource = [];
  331.         break;
  332.         case('tags'):
  333.         this.LIST.rebuildTagIndex();
  334.         if (delegate.selectedTag)
  335.         {
  336.             if (this.LIST.tagItemIndex[delegate.selectedTag])
  337.             {
  338.                 listSource = this.LIST.tagItemIndex[delegate.selectedTag];
  339.             break;
  340.             } else {
  341.             delegate.selectedTag = null;
  342.             }
  343.         }
  344.         // fall through to default
  345.         default:
  346.         listSource = this.LIST.list;
  347.         break;
  348.     }
  349.         
  350.         // Anything to filter out?
  351.         let filteredList = [];
  352.         if (filter) {
  353.         
  354.             let item, i=0;
  355.             let filterCaseIns = new RegExp(this.regexSafe(filter), 'i');
  356.             let tagFilter = new RegExp(this.regexSafe(filter)+'([^,]+)?($|,)', 'i');
  357.             
  358.         let c=0;
  359.             for( let i in listSource ) {
  360.  
  361.                 item = listSource[i];
  362.         
  363.         // Currently reading filter
  364.         if (typeOfList == 'current' && !item.percent) continue;
  365.         
  366.                 if (item.title.match( filterCaseIns ) ||
  367.                     item.url.match( filterCaseIns ) ||
  368.                     (item.tagList && item.tagList.match( tagFilter ))
  369.                     )
  370.                 {
  371.             filteredList[ c ] = item;
  372.             c++;
  373.                 }
  374.             }
  375.             
  376.         } else {
  377.             filteredList = listSource ? listSource.slice() : null; // make a copy of the object
  378.         }
  379.     
  380.     return filteredList;
  381.     
  382.     },
  383.     
  384.     sortList : function(listSource, sortValue)
  385.     {
  386.     
  387.     if (!listSource) return;
  388.     
  389.         switch( sortValue ) {
  390.             case('2'): //Oldest
  391.                 this.sortFunction = this.sortByDate;
  392.                 this.sortDirection = -1;
  393.                 break;
  394.             case('3'): //Title
  395.                 this.sortFunction = this.sortByTitle;
  396.                 this.sortDirection = 1;
  397.                 break;
  398.             case('4'): //Url
  399.                 this.sortFunction = this.sortByUrl;
  400.                 this.sortDirection = 1;
  401.                 break;
  402.             default: //Newest // should update offline queue component's version of this as well
  403.                 this.sortFunction = this.sortByDate;
  404.                 this.sortDirection = 1;
  405.                 break;
  406.         }
  407.         
  408.         var selfOBJECT = this;
  409.         listSource.sort( function(a,b){ return selfOBJECT.sortFunction(a,b) } );
  410.         
  411.         return listSource;
  412.         
  413.     },
  414.     
  415.     sortByDate : function(a,b) { // should update offline queue component's version of this as well
  416.         let r = a.timeUpdated < b.timeUpdated ? 1 : (a.timeUpdated > b.timeUpdated ? -1 : 0);
  417.         return r * this.sortDirection;
  418.     },
  419.     
  420.     sortByTitle : function(a,b) {
  421.         let r = a.title < b.title ? -1 : (a.title > b.title ? 1 : 0);
  422.         return r * this.sortDirection;
  423.     },
  424.     
  425.     sortByUrl : function(a,b) {
  426.         let r = a.url < b.url ? -1 : (a.url > b.url ? 1 : 0);
  427.         return r * this.sortDirection;
  428.     },
  429.     
  430.     
  431.     
  432.     
  433.     // -- Displaying List -- //    
  434.         
  435.     refreshRowInAllOpenWindows : function(itemId) {
  436.     this.commandInAllOpenWindows('RIL', 'refreshRow', itemId);
  437.     },
  438.     
  439.     refreshTagRowInAllOpenWindows : function(tags) {
  440.     this.commandInAllOpenWindows('RIL', 'refreshTagRow', tags);
  441.     },
  442.     
  443.     refreshListInAllOpenWindows : function(onlyForType) {
  444.     this.commandInAllOpenWindows('RIL', 'refreshList', onlyForType);
  445.     },    
  446.     
  447.     updateUnreadCount : function() {
  448.         this.commandInAllOpenWindows('RIL', 'updateUnreadCount');
  449.     },
  450.     
  451.     
  452.     
  453.     // -- FavIcons -- //
  454.     
  455.     fetchFavIconForItem : function(item, nsiuri)
  456.     {
  457.     // Get a NSIURI
  458.     if (!nsiuri)
  459.         nsiuri = this.uri(item.url);
  460.     
  461.     // Check if we've already looked
  462.     let favUrl = nsiuri.scheme+'://'+nsiuri.host+'/favicon.ico';
  463.     if (this.checkedFavIcons[ favUrl ]) return false;
  464.     this.checkedFavIcons[ favUrl ] = true;
  465.     
  466.     // Get the history service and register the observer if we haven't already
  467.     try {
  468.         if (!this.HISTORY)
  469.         this.HISTORY = Components.classes["@mozilla.org/browser/nav-history-service;1"].getService(Components.interfaces.nsINavHistoryService);
  470.         
  471.         if (!this.historyObserverAdded)
  472.         {
  473.         // Observer is removed in uninit and also after a set amount of time in populateList method
  474.         this.HISTORY.addObserver( this.historyObserver, false);
  475.         this.historyObserver.self = this;
  476.         this.historyObserverAdded = true;
  477.         }
  478.     } catch(e) { Components.utils.reportError(e); }
  479.     
  480.     this.ICO.setAndLoadFaviconForPage(nsiuri, this.uri(favUrl), false);
  481.     },
  482.     
  483.     favIconUpdated : function(aURI)
  484.     {    
  485.     let item = this.LIST.itemByUrl(aURI.spec);
  486.     if (item)
  487.         this.refreshRowInAllOpenWindows(item.itemId);
  488.     },
  489.     
  490.     // History observer
  491.     
  492.     historyObserver : {
  493.     onPageChanged: function(aURI, aWhat, aValue) {
  494.         if (aWhat == 3 && aValue.match('.ico'))
  495.         this.self.favIconUpdated(aURI)
  496.     },
  497.         
  498.         
  499.         // ----- not using these but they have to be defined:        
  500.     onBeginUpdateBatch: function() {},
  501.     onEndUpdateBatch: function() {},
  502.     onVisit: function(aURI, aVisitID, aTime, aSessionID, aReferringID,aTransitionType) {},
  503.     onTitleChanged: function(aURI, aPageTitle) {},
  504.     onDeleteURI: function(aURI) {},
  505.     onClearHistory: function() {},
  506.     onPageExpired: function(aURI, aVisitTime, aWholeEntry) {},
  507.     QueryInterface: function(iid) {
  508.         if (iid.equals(Components.interfaces.nsINavHistoryObserver) || iid.equals(Components.interfaces.nsISupports)) {
  509.         return this;
  510.         }
  511.         throw Cr.NS_ERROR_NO_INTERFACE;
  512.     }
  513.         //-----
  514.     },
  515.     
  516.     removeHistoryObserver : function()
  517.     {
  518.     if (this.historyObserverAdded && this.HISTORY)
  519.         this.HISTORY.removeObserver(this.historyObserver, false);
  520.     this.historyObserverAdded = false;
  521.         this.d('REMOVED HISTORY OBSERVER');
  522.     },
  523.  
  524.  
  525.  
  526.     // -- Logins -- //
  527.     
  528.     hasMasterPassword : function()
  529.     {
  530.         try
  531.         {
  532.             var tokendb = Components.classes["@mozilla.org/security/pk11tokendb;1"]
  533.                         .createInstance(Components.interfaces.nsIPK11TokenDB);
  534.             var token = tokendb.getInternalKeyToken();
  535.             
  536.             return token.needsLogin() && !token.isLoggedIn();
  537.         }catch(e){}
  538.     },
  539.     
  540.     getLogin : function()
  541.     {
  542.         if (this.PREFS.getBool('loggedOut')) return;
  543.         
  544.         let storeSecurely = this.PREFS.getBool('storeSecurely');
  545.         let hasMasterPassword = this.hasMasterPassword();
  546.         let promptedAboutMasterPass = this.PREFS.getBool('promptedAboutMasterPass');
  547.         
  548.         if (storeSecurely && hasMasterPassword && !promptedAboutMasterPass)
  549.         {
  550.             let check = {value:false};
  551.             this.PROMPT.alertCheck( this.getMainWindow(), "Master Passwords and Read It Later",
  552.                                     "Read It Later stores your username and password securely in Firefox's password manager. If you have a master password enabled this means you will be prompted every time RIL auto-syncs.  You can disable this under the advanced options by opting to disable storing your RIL account login securely.",
  553.                                     "Do not tell me again",
  554.                                     check);
  555.             if (check.value)
  556.             {
  557.                 this.PREFS.set('promptedAboutMasterPass', true);
  558.             }
  559.         }
  560.         
  561.         if (storeSecurely)
  562.         {   
  563.             // Find users for the given parameters
  564.             var logins = this.LOGIN.findLogins({}, this.loginInfo.hostname, this.loginInfo.formSubmitURL, this.loginInfo.httprealm);
  565.             return logins[0]; // only storing one, so just return the first
  566.         }
  567.         
  568.         else
  569.         {
  570.             let username = this.PREFS.get('username');
  571.             let password = this.PREFS.get('password');
  572.             
  573.             if (!username || !password) return;
  574.             
  575.             var nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
  576.                                  Components.interfaces.nsILoginInfo,
  577.                                  "init");
  578.     
  579.             return new nsLoginInfo(this.loginInfo.hostname,
  580.                                                     this.loginInfo.formSubmitURL, this.loginInfo.httprealm,
  581.                                                     username,
  582.                                                     password, "", "");
  583.             
  584.         }
  585.     },
  586.         
  587.     saveLogin : function(username, password)
  588.     {        
  589.         this.PREFS.set('loggedOut', false);
  590.     
  591.     // Remove login if it exists
  592.     let currentLogin = this.getLogin();
  593.     if (currentLogin)
  594.         this.LOGIN.removeLogin(currentLogin);
  595.     
  596.     // Save Login
  597.     var nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
  598.                                  Components.interfaces.nsILoginInfo,
  599.                                  "init");
  600.     
  601.         if (this.PREFS.getBool('storeSecurely'))
  602.         {
  603.             let loginInfo = new nsLoginInfo(this.loginInfo.hostname,
  604.                                                     this.loginInfo.formSubmitURL, this.loginInfo.httprealm,
  605.                                                     username,
  606.                                                     password, "", "");        
  607.             this.LOGIN.addLogin(loginInfo);
  608.             
  609.         this.removePrefLogin();
  610.             
  611.         }
  612.         
  613.         else
  614.         {
  615.             this.PREFS.set('username', username);
  616.             this.PREFS.set('password', password);
  617.         }
  618.     },
  619.     
  620.     logout : function(quiet)
  621.     {        
  622.     var login = this.getLogin();
  623.     if (login)
  624.     {
  625.             if (this.PREFS.getBool('storeSecurely'))
  626.                 this.LOGIN.removeLogin(login);
  627.             else
  628.                 this.removePrefLogin();
  629.     }
  630.         
  631.         this.PREFS.set('loggedOut', true);
  632.     
  633.     if (!quiet)
  634.         this.PROMPT.alert(this.getMainWindow(true), 'Read It Later', 'You have been logged out.  Changes to your reading list will no longer be synced.');
  635.     },
  636.     
  637.     removePrefLogin : function()
  638.     {
  639.         // Remove passwords from prefs
  640.     this.PREFS.remove('username');
  641.     this.PREFS.remove('password');
  642.     },
  643.     
  644.     relogin : function()
  645.     {
  646.     this.commandInTopRIL('relogin');
  647.     },
  648.     
  649.     // -- //
  650.     
  651.    
  652.     autoSyncIdleObserver : {
  653.     observe: function(subject, topic, data) {
  654.         if (topic == 'idle')
  655.         {
  656.         this.isIdle = true;
  657.         } else if (topic == 'back' && this.isIdle)
  658.         {
  659.         this.syncWhenListIsOpened = true;
  660.         }
  661.     }
  662.     },
  663.     
  664.     observe: function(subject, topic, data)
  665.     {        
  666.         switch(topic)
  667.         {
  668.             case('ril-api-request-finished'):
  669.                 this.SYNC.requestCallback(subject, data);
  670.                 break;
  671.         }
  672.     },
  673.     
  674.     
  675.     // -- Offline -- //    
  676.     
  677.     updateDownloadProgress : function(pointer, size)
  678.     {
  679.     this.commandInAllOpenWindows('RIL', 'updateDownloadProgress', [pointer,size], true);
  680.     },
  681.        
  682.     
  683.     // -- Link Resolving -- //
  684.     
  685.     resolveLink : function(itemId, url, callback)
  686.     {
  687.     let listener = new this.listenerResolver(itemId, url, this, 'resolveLinkCallback', this);
  688.     listener.start();    
  689.     },
  690.     
  691.     resolveLinkCallback : function(itemId, url, newUrl)
  692.     {
  693.     let item = this.LIST.itemById(itemId);
  694.     if (!item) return; //if item was removed during the resolving
  695.     
  696.     if (url != newUrl)
  697.     {        
  698.         // URL has changed
  699.         this.LIST.changeURL( itemId, newUrl );
  700.     }
  701.     },
  702.     
  703.     listenerResolver : function(itemId, url, delegate, selector, APP) {
  704.     this.itemId = itemId;
  705.     this.url = url;
  706.     this.delegate = delegate;
  707.     this.selector = selector;
  708.     this.APP = APP;
  709.     }, //-- prototype defined below -- //
  710.         
  711.     definePrototypes : function()
  712.     {    
  713.     this.listenerResolver.prototype =
  714.     {
  715.         start : function()
  716.         {
  717.         // Open connection to resolve the link
  718.         let request = this.request = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Components.interfaces.nsIXMLHttpRequest);  
  719.         request.open("GET", this.url, true);        
  720.         request.onreadystatechange = this.APP.genericClosure(this, 'onreadystate');
  721.         request.send();
  722.         },
  723.         
  724.         onreadystate : function(e)
  725.         {
  726.         if (this.request.readyState >= 2 && !this.finished)
  727.         {
  728.             this.finish();
  729.         }        
  730.         },
  731.         
  732.         finish : function()
  733.         {
  734.         this.finished = true;
  735.         this.delegate[this.selector].call( this.delegate, this.itemId, this.url, this.request.channel.URI.spec );
  736.         }
  737.     };
  738.     },
  739.     
  740.     
  741.     // --  Observers -- //
  742.     
  743.     registerObserver : function(topic, delegate) {
  744.     this.OBSERVER.addObserver(delegate ? delegate : this, topic, false);
  745.     },
  746.     unregisterObserver : function(topic, delegate) {
  747.     this.OBSERVER.removeObserver(delegate ? delegate : this, topic);
  748.     },
  749.     
  750.     
  751.     // -- Offline -- //
  752.     
  753.     offlinePathHasChanged : function()
  754.     {
  755.     this.ASSETS.init();
  756.     },
  757.     
  758.     updateOfflineQueue : function(newItems)
  759.     {
  760.     let item;
  761.     for(let i in newItems)
  762.     {
  763.         item = this.LIST.itemById(newItems[i]);
  764.         if (item)
  765.         this.OFFLINE.addItemToQueue(item, null, true);
  766.     }
  767.     },
  768.     
  769.     
  770.     // -- Helpers -- //  
  771.     
  772.     commandInAllOpenWindows : function(objectName, methodName, argument, notInSidebar, any) {
  773.     
  774.     let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]  
  775.                .getService(Components.interfaces.nsIWindowMediator);  
  776.     let enumerator = wm.getEnumerator( any ? null : 'navigator:browser');  
  777.     while(enumerator.hasMoreElements()) {  
  778.         let win = enumerator.getNext();
  779.         if (win[objectName]) {
  780.         
  781.         // Update row in window (dropdown)
  782.         win[objectName][methodName](argument);
  783.         
  784.         // Update row in sidebar
  785.         if (!notInSidebar && win[objectName].xulId('sidebar', true) &&
  786.             win[objectName].xulId('sidebar', true).contentWindow[objectName]) {
  787.             win[objectName].xulId('sidebar', true).contentWindow[objectName][methodName](argument);
  788.         }
  789.         }
  790.     }
  791.     },
  792.     
  793.     commandInMainWindow : function(objectName, methodName, arg1, arg2, arg3, arg4, arg5, arg6)
  794.     {
  795.         let mainWindow = this.getMainWindow();
  796.         if (mainWindow && mainWindow[objectName])
  797.             mainWindow[objectName][methodName](arg1, arg2, arg3, arg4, arg5, arg6);        
  798.     },
  799.     
  800.     commandInTopRIL : function(methodName, arg1, arg2, arg3, arg4, arg5, arg6)
  801.     {
  802.         let mainWindow = this.getMainWindow();
  803.     if (mainWindow && mainWindow.RIL)
  804.     {
  805.         let topRIL = mainWindow.RIL.getPriorityRIL();
  806.         if (topRIL)
  807.         topRIL[methodName](arg1, arg2, arg3, arg4, arg5, arg6);        
  808.     }
  809.     },
  810.     
  811.     
  812.     // -- Delegate to the main window functions -- //
  813.     
  814.     openUrl : function(url, offline, targ, ref) {
  815.     this.commandInMainWindow( 'RIL' , 'openUrl', url, offline, targ, ref);
  816.     },
  817.     
  818.     openSitePage : function(page, login)
  819.     {    
  820.     let url = 'http://readitlaterlist.com/';
  821.     let currentLogin = this.getLogin();
  822.     let postData;
  823.     
  824.     if (login && currentLogin)
  825.     {
  826.         let params = 'username='+this.e(currentLogin.username)+'&password='+this.e(currentLogin.password);
  827.  
  828.         url += 'goto?page='+page;
  829.         
  830.         let stringStream = Components.classes["@mozilla.org/io/string-input-stream;1"].createInstance(Components.interfaces.nsIStringInputStream);
  831.         
  832.         if ("data" in stringStream) // Gecko 1.9 or newer
  833.         stringStream.data = params;
  834.         else // 1.8 or older
  835.         stringStream.setData(params, params.length);
  836.         
  837.         postData = Components.classes["@mozilla.org/network/mime-input-stream;1"].createInstance(Components.interfaces.nsIMIMEInputStream);
  838.         postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
  839.         postData.addContentLength = true;
  840.         postData.setData(stringStream);
  841.  
  842.     }    
  843.     else
  844.     {
  845.         url += page;    
  846.     }
  847.     
  848.     /*this.getMainWindow().openDialog(url, '_blank', 'all,dialog=no,chrome=no',
  849.                 null, null, null, postData);*/
  850.     
  851.     // Not ideal, would rather open this in a new window
  852.     let mainWindow = this.getMainWindow();
  853.     let gBrowser = mainWindow.getBrowser();
  854.     gBrowser.selectedTab = gBrowser.addTab(url, null, null, postData )
  855.     mainWindow.focus( );
  856.  
  857.     
  858.     },
  859.     
  860.     genericMessage : function(msg, buttons, openWindow, chooserValue, persist, inPlaceOfList)
  861.     {
  862.     this.commandInTopRIL( 'genericMessage', msg, buttons, openWindow, chooserValue, persist, inPlaceOfList);
  863.     },
  864.     
  865.     
  866.     
  867.     // -- Document Helpers -- //    
  868.     
  869.     getMainWindow : function(any)
  870.     {
  871.     let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]  
  872.                         .getService(Components.interfaces.nsIWindowMediator);  
  873.         return wm.getMostRecentWindow( any ? null : "navigator:browser"); 
  874.     },
  875.     
  876.     getTopRIL : function()
  877.     {
  878.     return this.getMainWindow().RIL;
  879.     },
  880.     
  881.     setTimeout : function(func, time, that, interval, timer)
  882.     {
  883.         that = that ? that : this;
  884.         let callback = {that:that, notify:function(){func.call(this.that)}};
  885.         
  886.         let timer = timer ? timer : Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
  887.         timer.initWithCallback(callback, time, interval ? Components.interfaces.nsITimer.TYPE_REPEATING_SLACK : Components.interfaces.nsITimer.TYPE_ONE_SHOT);
  888.         return timer;
  889.     },
  890.     
  891.     setInterval : function(func, time, that)
  892.     {
  893.         return this.setTimeout(func, time, that, true);
  894.     },
  895.     
  896.     clearTimeout : function(timer)
  897.     {
  898.         if (timer)
  899.     {
  900.         timer.cancel();
  901.         return timer;
  902.     }
  903.     },
  904.     
  905.     
  906.     // -- Helpers -- //    
  907.       
  908.     stripTags : function(str){
  909.     return str.replace(/<\/?[^>]+>/gi, '');        
  910.     },
  911.     
  912.     e : function(str) {
  913.             return encodeURIComponent(str);
  914.     },
  915.     et : function(title) {
  916.             return this.e(title.replace(/"/g,'"'));
  917.     },
  918.     
  919.     uri : function(uri) {
  920.     return this.IO.newURI(uri, null, null);
  921.     },
  922.     
  923.     checkIfValidUrl : function(url, alert)
  924.     {
  925.     let valid = true;    
  926.     
  927.     if (url.length > 1000 || url.match(/^data:.*;base64/))
  928.         valid = false; // data, will cause freeze if it has to be parsed
  929.     
  930.     if (valid)
  931.     {
  932.         let parsed = this.parseUri(url);
  933.         if (parsed.protocol == 'http' || parsed.protocol == 'https') {
  934.         return true;
  935.         }
  936.     }
  937.     
  938.     //else
  939.     if (alert) {
  940.         this.PROMPT.alert(this.getMainWindow(), 'Read It Later', this.getTopRIL().l('OnlyWebsites') );
  941.         return false;
  942.     }
  943.     },
  944.     
  945.     parseUrl : function(url, forLookup, noCheckOnFail) { 
  946.     if (url) {
  947.             if (url.length > 1250) return; //too long of a url locks browser in parseUri script
  948.             
  949.         try {
  950.         /**********
  951.         Updates to this logic need to be updated in the parse url server side function
  952.         ***********/
  953.         
  954.         //forLookup - remove anchor (unless it has slashes) - decodeURI
  955.         
  956.         let parsed = this.parseUri( forLookup ? decodeURI(url) : url );
  957.         
  958.         parsed.host = parsed.host.toLowerCase().replace('www.', ''); //remove www. and make domain lowercase
  959.         parsed.path = parsed.path.replace(new RegExp('/$'), '');  //remove trailing slash
  960.         
  961.                 anchorPart = parsed.anchor && (!forLookup || parsed.anchor.match(/\//)) ? ('#'+parsed.anchor) : ''; 
  962.                 
  963.         return parsed.protocol + '://' + parsed.host + parsed.path + parsed.query + anchorPart;
  964.         } catch(e)
  965.         {
  966.         Components.utils.reportError(e);
  967.         Components.utils.reportError('url from previous error: ' + url);
  968.         
  969.         if (url.length > 250 && !noCheckOnFail)
  970.         {
  971.             // might have been truncated, look to see if it ends in a broken %XX encoding
  972.             let regex = /%[0-9]{0,1}$/;
  973.             if (url.match(regex))
  974.             return this.parseUrl(url.replace(regex,''), forLookup, true);
  975.         }
  976.                 
  977.         }
  978.     }
  979.     },
  980.     
  981.     parseUri : function(urlStr, baseURL) // update this in ASSET MANAGER as well
  982.     {
  983.     if (baseURL && !urlStr.match(/^https?:/))
  984.     {
  985.         // convert urlStr into a full absolute url
  986.         let parsedBase = this.parseUri(baseURL);
  987.         let baseRoot = parsedBase.protocol + '://' + parsedBase.authority + '/';
  988.         
  989.         if ( urlStr.match(/^\.\.?\//) ) // ../../format.html
  990.         {
  991.         let relativeBaseParts = parsedBase.relative.split('/');
  992.         if (relativeBaseParts.length < 3)
  993.         {
  994.             urlStr = baseRoot + urlStr.replace(/^(\.\.\/){0,}/,'')
  995.         }
  996.         else
  997.         {
  998.             relativeBaseParts.shift(); //remove first which will be empty  (x)/something/something/
  999.             if (parsedBase.file || relativeBaseParts[relativeBaseParts.length-1].length==0) relativeBaseParts.pop(); //remove end (file)
  1000.             
  1001.             let end = relativeBaseParts.length - urlStr.match(/\.\.\//g).length;
  1002.             let rel = relativeBaseParts.slice(0,end>0?end:0).join('/');
  1003.             urlStr = baseRoot + (rel ? rel+'/' : '') + urlStr.replace(/^(\.\.\/){0,}/,'');             
  1004.         }
  1005.         
  1006.         }
  1007.         
  1008.         else if (urlStr.match(/^\//)) // /format.html
  1009.         {
  1010.         urlStr = baseRoot.replace(/\/$/,'') + urlStr;
  1011.         }
  1012.         
  1013.         else
  1014.         { // format/format.html
  1015.         urlStr = baseRoot.replace(/\/$/,'') + parsedBase.directory.replace(/[^\/]*$/,'') + urlStr;
  1016.         }
  1017.         
  1018.     }
  1019.     
  1020.         var o = {
  1021.             strictMode: false,
  1022.             key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
  1023.             q:   {
  1024.                 name:   "queryKey",
  1025.                 parser: /(?:^|&)([^&=]*)=?([^&]*)/g
  1026.             },
  1027.             parser: {
  1028.                 strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
  1029.                 loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
  1030.             }
  1031.         }
  1032.         
  1033.         var m    = o.parser[o.strictMode ? "strict" : "loose"].exec(urlStr),
  1034.             uri = {},
  1035.             i   = 14;
  1036.         
  1037.         while (i--) uri[o.key[i]] = m[i] || "";
  1038.         
  1039.         uri[o.q.name] = {};
  1040.         uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
  1041.             if ($1) uri[o.q.name][$1] = $2;
  1042.         });
  1043.         
  1044.     uri.spec = urlStr;
  1045.     uri.scheme = uri.protocol;
  1046.     
  1047.         return uri;        
  1048.     },
  1049.     
  1050.     now : function() {
  1051.     let d = new Date();
  1052.     return d.getTime()/1000;
  1053.     },
  1054.     ar : function(a){
  1055.     let str = '';
  1056.     for(let i in a){
  1057.         str += i + ' : ' + ( typeof a[i] == 'object' ? this.ar(a[i], true) : a[i] ) + "\n";
  1058.     }
  1059.     return str;
  1060.     },
  1061.     d : function(str)
  1062.     {
  1063.         return dump(str+"\n");
  1064.     },
  1065.     stripTags : function(str){
  1066.     return str.replace(/<\/?[^>]+>/gi, '');        
  1067.     },
  1068.     trim : function(str)
  1069.     {
  1070.     if (!str) return '';
  1071.         return str.trim();
  1072.     },
  1073.     trimLeft : function(str)
  1074.     {
  1075.         if (!str) return '';
  1076.     return str.trimLeft();
  1077.     },
  1078.     regexSafe : function(str)
  1079.     {
  1080.     return str.replace(/(\W)/g, '\\$1');    
  1081.     },    
  1082.     
  1083.     genericClosure : function(delegate, selector)
  1084.     {
  1085.     if (!delegate || !selector) return;
  1086.     var method = delegate[selector];
  1087.     function closure(a,b,c,d,e,f){ method.call(delegate,a,b,c,d,e,f); };
  1088.     return closure;
  1089.     },  
  1090.     
  1091.     genericDataClosure : function(delegate, selector, data)
  1092.     {
  1093.     if (!delegate || !selector) return;
  1094.     var method = delegate[selector];
  1095.         var dataVar = data;
  1096.     function closure(){ method.call(delegate,dataVar); };
  1097.     return closure;
  1098.     },
  1099.     
  1100.     
  1101.        
  1102.     /* --- Migrating from 0.9 to 2.0 --- */
  1103.     
  1104.     upgradeFromBeta : function()
  1105.     {    
  1106.     try {
  1107.         
  1108.         // Get folder id (old pref from pre 2.0)
  1109.         let folderId = this.PREFS.get('folderId');
  1110.         if (!folderId) return false; // no need to upgrade
  1111.         
  1112.         
  1113.         // -- Handle old logins
  1114.         
  1115.         // cancel auto sync
  1116.         this.hasSyncedAtStartup = true;
  1117.         
  1118.         // Open login dialog?
  1119.         if (this.PREFS.getBool('feed') && this.PREFS.getBool('sync'))
  1120.         {
  1121.         // User is already syncing, store current login details, do not present login dialog
  1122.         
  1123.         // get old details
  1124.         let username = this.PREFS.get('feed-id-'+this.PREFS.get('feed-which'));
  1125.         let password = this.PREFS.get('sync-'+this.PREFS.get('feed-which'));
  1126.         
  1127.         // save details
  1128.         if (username && password)
  1129.             this.saveLogin(username, password);
  1130.         
  1131.         }
  1132.         
  1133.         // Remove passwords from prefs
  1134.         //this.PREFS.remove('sync-default');
  1135.         //this.PREFS.remove('sync-alt');
  1136.             
  1137.             // Update old prefs
  1138.             try
  1139.             {
  1140.                 this.PREFS.setIfDoesNotExist( 'autoCloseTab', this.PREFS.get('auto-close-tab') == 'true' );
  1141.                 this.PREFS.set( 'autoCloseTab', this.PREFS.get('auto-close-tab') == 'true' );
  1142.                 this.PREFS.set("list-type",    'pages');
  1143.             }
  1144.             catch(e)
  1145.             {}
  1146.         
  1147.         
  1148.         // -- Transfer list from bookmarks into new database
  1149.         
  1150.         
  1151.         // Retrieve link resolver
  1152.         let resolver = {};
  1153.         try
  1154.         {        
  1155.         // Connect to old db
  1156.         let file = Components.classes["@mozilla.org/file/directory_service;1"]
  1157.                         .getService(Components.interfaces.nsIProperties)
  1158.                         .get("ProfD", Components.interfaces.nsIFile);
  1159.         file.append("ril.sqlite");    
  1160.         let db = this.STORAGE.openDatabase(file);
  1161.                 
  1162.         // query old db and build resolver
  1163.         let sql = "SELECT id, original_url FROM ril_link_resolver"
  1164.         let statement = db.createStatement(sql);
  1165.         let id, url;
  1166.         while (statement.executeStep())
  1167.         {
  1168.             id = statement.getInt32(0);
  1169.             url = statement.getUTF8String(1);
  1170.                 
  1171.             resolver[ id ] = url;
  1172.         }
  1173.         statement.reset();        
  1174.         
  1175.         } catch(e) {
  1176.         Components.utils.reportError(e);
  1177.         }
  1178.         
  1179.         
  1180.         // Retreive old list and add to new system in batch mode
  1181.         let sHistory =    Components
  1182.                 .classes["@mozilla.org/browser/nav-history-service;1"]
  1183.                 .getService(Components.interfaces.nsINavHistoryService);            
  1184.         
  1185.            
  1186.         // Search Bookmarks 
  1187.         let options = sHistory.getNewQueryOptions();
  1188.         let query = sHistory.getNewQuery();
  1189.         query.setFolders([folderId], 1);
  1190.           
  1191.         let listResult = sHistory.executeQuery(query, options);
  1192.     
  1193.         let rootNode = listResult.root;
  1194.         rootNode.containerOpen = true;        
  1195.  
  1196.         // Process Results
  1197.         let PU = this.getMainWindow().PlacesUtils;
  1198.         for (let i = 0; i < rootNode.childCount; i ++) {
  1199.         let node = rootNode.getChild(i);
  1200.         if (PU.nodeIsBookmark(node))
  1201.         {
  1202.             let itemId, item;
  1203.             
  1204.             itemId = this.LIST.add({
  1205.             url: node.uri,
  1206.             title: node.title,
  1207.             timeUpdated: node.dateAdded / 1000 / 1000
  1208.             }, true, true, true);
  1209.             
  1210.             if (node.tags)
  1211.             this.LIST.saveTags(itemId, node.tags, true, true);
  1212.             
  1213.             
  1214.             if (!itemId)
  1215.             item = this.LIST.itemByUrl(node.uri);
  1216.             
  1217.             if (resolver[node.itemId])
  1218.             this.LIST.addUrlToResolverForItemId(item ? item.itemId : itemId, resolver[node.itemId], false, true);
  1219.         }
  1220.             
  1221.         }
  1222.         this.LIST.flushBatch();
  1223.     
  1224.         // close a container after using it!
  1225.         rootNode.containerOpen = false;
  1226.         
  1227.         
  1228.         // upgraded successful, remove old folder id
  1229.         this.PREFS.remove('folderId');
  1230.         
  1231.         // hard sync
  1232.             if (this.getLogin())
  1233.                 this.SYNC.sync(true);
  1234.         
  1235.         return true;
  1236.     
  1237.     } catch(e) {
  1238.         Components.utils.reportError(e);
  1239.     }
  1240.     
  1241.     }
  1242.  
  1243. };
  1244.  
  1245.  
  1246.  
  1247. var components = [RILdelegate];
  1248. function NSGetModule(compMgr, fileSpec) {
  1249.   return XPCOMUtils.generateModule(components);
  1250. }
  1251.